iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 7

days[6] = "三顆渲染樹是如何運作的?(三)"

  • 分享至 

  • xImage
  •  

接下來我們就要來實際走訪一次整個渲染流程,看看Flutter App是怎麼啟動,三顆渲染樹是怎麼從無到有被建立起來,又是怎麼更新的。

首先介紹一下這次要使用的範例Widget Tree:

void main() {
  runApp(Container(
    alignment: Alignment.topCenter,
    child: MyTimer(),
  ));
}

class MyTimer extends StatefulWidget {
  @override
  _MyTimerState createState() => _MyTimerState();
}

class _MyTimerState extends State<MyTimer> {
  DateTime _currentDateTime = DateTime.now();

  @override
  void initState() {
    super.initState();
    Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        _currentDateTime = DateTime.now();
      });
    });

  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: RichText(
        text: TextSpan(text: _currentDateTime.toString()),
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

簡單來說這個App會每秒更新現在時間,顯示在畫面正中央。看起來比之前的範例複雜許多,但其實它最終也只有Container, Align, MyTimer, Padding, RichText等五個Widget而已

  • Container: StatelessWidget,作為Widget Composition Expansion的代表。有時候當你包一層Widget上去,它在build時其實會幫你多包好幾層(如MaterialApp, Scaffold),也就是一種解壓縮的概念。Container是其中最常用也最單純的Widget。
  • Align: RenderObjectWidget,從Container解壓縮出來的Widget。這裡可以看到當底下的MyTimer更新時,它的Widget, Element, RenderObject不會有任何改變。
  • MyTimer: StatefulWidget,主要就是為了觀察State如何被建立和更新。雖然為了StatefulWidget要多寫一些code,但內建的StatefulWidget都相對複雜一點,不如自己寫也看得比較清楚。
  • Padding: RenderObjectWidget,用來示範當MyTimer更新時,這裡的Widget雖然會重建,但Element, RenderObject不會有任何改變。
  • RichText: RenderObjectWidget,作為Widget重建,Element, RenderObject更新的範例。

在開始之前,強烈建議你把範例APP跑起來,用Debug Mode跟著走一次,對於文章內容會更有感受。因為這篇真的很長,相信你不會想再看第二次。好,讓我們開始吧。程式從runApp進入scheduleAttachRootWidget,到達attachRootWidget:

  // In WidgetsBinding
  void attachRootWidget(Widget rootWidget) {
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( // 1.
      container: renderView, // 3.
      debugShortDescription: '[root]',
      child: rootWidget, // 2.
    ).attachToRenderTree( // 4.
      buildOwner, 
      renderViewElement as RenderObjectToWidgetElement<RenderBox>,
    );
  }
  1. 這裡的RenderObjectToWidgetAdapter不要被它嚇到以為是什麼Adapter,它其實還是一個Widget,也就是整顆Widget Tree真正的Root Widget。
  2. 一個Widget自然可以有child,而它的child,rootWidget參數,就是我們從runApp傳入的那一包。
  3. container, renderView,兩個都是沒看過的字,到底什麼意思?其實RenderView是一種特殊的RenderObject,也就是作為整個RenderObject Tree真正的root,它的實例是由Framework提供的,我們等一下會看到它被"建立"。
  4. 最後這個root widget會呼叫attachToRenderTree,得到回傳值給予_renderViewElement,這就是我們的Element Tree的root。

此時候的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053ixY6RzovVC.png


接下來進入attachToRenderTree:

  // In RenderObjectToWidgetAdapter
  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement(); // 1.
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null); // 2.
      });
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }
  1. 這裡由RenderObjectToWidgetAdapter呼叫createElement,自然是RenderObjectToWidgetElement。
  2. 這裡呼叫element.mount(null, null),因為這個Element正是Root Element,沒有Element也沒有slot可以mount。呼叫mount的用意是為了觸發updateChild。

此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053ZgzMoxdTTs.png


讓我們進入mount看看:

  // In Element
  @mustCallSuper
  void mount(Element parent, dynamic newSlot) {
    ....
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    ....
  }

這裡我們先經過幾層super.mount到達最上層,看到mount最核心的工作,就是設定此一Element被掛在哪個parent的哪個slot上。

接著我們回到上一層的RenderObjectElement:

  // In RenderObjectElement
  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot); // 1.
    ....
    _renderObject = widget.createRenderObject(this); // 2.
    ....
    attachRenderObject(newSlot); // 3.
    _dirty = false; // 4.
  }
  1. 這是剛剛呼叫的super.mount
  2. 還記得這個widget是RenderObjectToWidgetAdapter,並接收一個renderView做成員變數嗎?這裡的createRenderObject只是回傳了那個renderView而已。
  3. 這裡因為是Root Element和Root RenderObject,其實沒做什麼事,我們之後會再回來看它。
  4. 剛建立剛被掛載的Element當然不是dirty的。

此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053qEMnLgYC8u.png


最後我們回到RenderObjectToWidgetElement的mount:

  static const Object _rootChildSlot = Object(); // 3
  
  @override
  void mount(Element parent, dynamic newSlot) {
    assert(parent == null);
    super.mount(parent, newSlot);
    _rebuild(); // 1.
  }
  void _rebuild() {
    try {
      _child = updateChild(_child, widget.child, _rootChildSlot); // 2
      assert(_child != null);
    } catch (exception, stack) {
      ...
    }
  }
  1. 裡面只剩一個rebuild,主要功用是呼叫updateChild。
  2. 這裡傳入此Element的Child,以及此Element對應的widget的child,還有此Element的一個slot。我們現在終於可以揭開slot的神秘面紗了。
  3. 原來slot只是一個平凡無奇,毫無反應的Object,主要就只是用物件比對來判斷這個slot有沒有被佔用。

讓我們繼續進入updateChild:

// |              | newWidget == null              | newWidget != null      |
// |child == null | Returns null.                  | Returns new [Element]. |
// |child != null | Remove old child, returns null | Old child updated if possible, returns child or new [Element]. |
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ....
    if (child != null) {
      ....
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }
    ....
  }

整個Widget系統的核心邏輯就在這裡,code很複雜我就不貼了,直接看註解也滿容易懂的。對當前這個Parent Element而言:

  1. 如果本來沒有child,其對應位置也沒有newWidget,就不須要建立新的Child Element
  2. 如果本來沒有child,但其對應位置出現newWidget,就建立新的Child Element
  3. 如果本來有child,其對應位置的newWidget被移除了,就跟著移除Child Element
  4. 如果本來有child,其對應位置又有newWidget,就試著更新原本的Child Element,如果不行則建立新的。

我們之後會再看到4.是怎麼判斷更新或新建的。記得我們現在還在RenderObjectToWidgetElement嗎?這時候它還沒有child,newWidget則是我們最一開始從runApp傳入的Container(child:...),因此我們將進入2.,也就是inflateWidget的部份。

  // In Element
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ....
    final Element newChild = newWidget.createElement();
    ....
    newChild.mount(this, newSlot);
    ....
    return newChild;
  }

inflateWidget最主要的工作就是透過Widget建立Element,並將它掛載到自己的一個slot上。這跟之前的attachToRenderTree是很相似的,只是attachToRenderTree是屬於Root Element的特殊情況,而這邊是開始遞迴的一般情況。這時的newWidget是Container,因此將產生StatelessElement來掛載,此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053DjPGzoKJZj.png


我們再次進入mount:

  In ComponentElement
  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_active);
    _firstBuild();
    assert(_child != null);
  }

記得這時候的mount是來自Container的StatelessElement(ComponentElement),經過_firstBuild() -> rebuild() -> performRebuild()之後終於能找到我們最熟悉的build函數了:

  // In ComponentElement
  @override
  void performRebuild() {
      ....
      built = build();
      ....
      _child = updateChild(_child, built, slot);
      ....
  }

馬上來看看Container的build長什麼樣:

  // In Container
  @override
  Widget build(BuildContext context) {
    Widget current = child;
    ....
    if (alignment != null)
      current = Align(alignment: alignment, child: current);

    final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
    if (effectivePadding != null)
      current = Padding(padding: effectivePadding, child: current);

    if (color != null)
      current = ColoredBox(color: color, child: current);
    ....
    return current;
  }

這裡只留下幾個常用屬性的邏輯,感受一下Container的運作方式,其它都大同小異。我們可以看到,因為我們有設定alignment屬性,我們傳入的child(MyTimer)被多包了一層Align才回傳。此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053V4r3Gtb7vp.png


重複的程式碼我就不再貼了,可以隨時拉回去參考。build完之後我們再次進入updateChild,這次的parent是Container,child是Align,同樣因為沒有Element而進入

newChild = inflateWidget(newWidget, newSlot);

inflateWidget中會呼叫Align的createElement,得到SingleChildRenderObjectElement後再次進行mount。此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/20129053TeR6gUYTH6.png


同樣是RenderObjectElement的mount

  void mount(Element parent, dynamic newSlot) {
    ....
    _renderObject = widget.createRenderObject(this);
    ....
    attachRenderObject(newSlot);
    ....
  }

此時的widget是Align,createRenderObject會建立RenderPositionedBox。在它被掛載之前,我們先把它放在旁邊:https://ithelp.ithome.com.tw/upload/images/20200907/20129053lJsBjY7ns5.png


這是因為這次的attachRenderObject就有趣了,讓我們來看看:

  @override
  void attachRenderObject(dynamic newSlot) {
    assert(_ancestorRenderObjectElement == null);
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); // 1.
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot); // 2.
    ....
  }
  1. 首先要找到Element Tree中上一個屬於RenderObjectElement的節點,這裡是RenderObjectToWidgetElement。
  2. 在這個節點的對應的RenderObject(RenderView)下插入新的RenderObject(RenderPositionedBox)
    此時的三渲染樹:https://ithelp.ithome.com.tw/upload/images/20200907/201290536aIpOBht9F.png

好的時間又不夠了,如果你能看到這裡我只能說你真是太神啦!但是讓我們彼此都休息一下吧,我們已經跑了一半囉!其實後面很多都大同小異了,只剩下State的建立和更新機制,應該會再花一些篇幅,我們下次再繼續吧。


上一篇
days[5] = "三顆渲染樹是如何運作的?(二)"
下一篇
days[7] = "三顆渲染樹是如何運作的?(四)"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言